package nl.scalasim

import java.lang.Math._
import scala.collection.mutable.ArrayBuffer

/**
 * A TileMap represents a geographical map within the game.
 *
 * @author Yigal Duppen
 * @since Oct 1, 2010
 *
 * @param width - the width of the map (along the 3D x-axis)
 * @param depth - the depth of the map (along the 3D z-axis)
 */
class TileMap extends AnyRef
        with Logging
{

  // for now, this is how we define a map :)
  // legenda:
  //    .   ground
  //    ^   mountain
  //    ~   water

  private val map_init = """
^^^^^^^^^^^^....................................................................
^^^^^^^^^^^^....................................................................
^^^^^^^^^^^^....................................................................
^^^^^^^^^^^^....................................................................
^^^^^^^^^^^^....................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
................................................................................
~........................................^^^^^^^^^^^^...........................
~~.......................................^^^^^^^^^^^^...........................
~~~......................................^^^^^^^^^^^^...........................
~~~~~~~~~~...............................^^^^^^^^^^^^...........................
~~~~~~~~~~~~~............................^^^^^^^^^^^^...........................
~~~~~~~~~~~~~~..................................................................
~~~~~~~~~~~~~~~~~~..............................................................
~~~~~~~~~~~~~~~~~~..............................................................
~~~~~~~~~~~~~~~.................................................................
~~~~~~~~~~~~~~~~~...............................................................
~~~~~~~~~~~~~~..................................................................
~~~~~~~~~~~~~...................................................................
~~~~~~~~~~~~....................................................................
~~~~~~~~~~~.....................................................................
~~~~~~~~~~~~~...................................................................
~~~~~~~~~~~~....................................................................
~~~~~~~~~~~~....................................................................
~~~~~~~~~~~~....................................................................
~~~~~~~~~~......................................................................
~~~~~~~~~.......................................................................
~~~~~~~~~~~.................................................................~~~~
~~~~~~~~~~...............................................................~~~~~~~
~~~~~~~~~..............................................................~~~~~~~~~
~~~~~~~~.............................................................~~~~~~~~~~~
~~~~~~...............................................................~~~~~~~~~~~
~~~~~~~~~.............................................................~~~~~~~~~~
~~~~~~~~~~~............................................................~~~~~~~~~
""".trim

  val width = 80
  val depth = 40

  private val tilecount = width * depth
  private val tilemap = new ArrayBuffer[Tile](tilecount)
  private val pathfinder = new AStar[Int](cost, h, neighbours.apply)
  private val neighbours = new ArrayBuffer[Set[Int]](tilecount)

  @inline
  private def internal(x: Int, y: Int): Int = y * width + x

  @inline
  private def internal(p: Position): Int = internal(p._1, p._2)

  @inline
  private def external(n: Int): Position = (n % width, n / width)

  map_init foreach {
    c =>
      c match {
        case '~' => tilemap += new Tile(Water)
        case '^' => tilemap += new Tile(Mountains)
        case '.' => tilemap += new Tile(Normal)
        case '\n' => ()
        case n => log.severe("UNKNOWN TILE: " + n)
      }
  }

  tilemap.indices foreach {
    i =>
      neighbours += calc_neighbours(i)
  }

  private def calc_neighbours(from: Int): Set[Int] = {
    def n(x: Int, y: Int) = internal(min(width - 1, max(0, x)), min(depth - 1, max(0, y)))

    val (x, y) = external(from)

    Set(n(x - 1, y - 1), n(x, y - 1), n(x + 1, y - 1),
      n(x - 1, y), n(x + 1, y),
      n(x - 1, y + 1), n(x, y + 1), n(x + 1, y + 1))
  }

  private def cost(from: Int, to: Int): Int = {
    (tilemap(from).terrain, tilemap(to).terrain) match {
      case (Mountains, _) => 1000
      case (_, Mountains) => 1000
      case (Water, _) => 1000
      case (_, Water) => 1000
      case (_, _) => h(from, to)
    }
  }

  private def h(from: Int, to: Int): Int = {
    val (x1, y1) = external(from)
    val (x2, y2) = external(to)

    @inline def sq(n: Int) = n * n

    ceil(sqrt(sq(x1 - x2) + sq(y1 - y2))).toInt
  }

  /**
   * Obtain the Tile at the specified position.
   * @return None if this position does not exist, Some(tile) otherwise. 
   */
  def apply(x: Int, y: Int): Option[Tile] = {
    val i = internal(x, y)
    if (tilemap.isDefinedAt(i)) {
      Some(tilemap(i))
    }
    else {
      None
    }
  }

  /**
   * @see apply ( Int, Int )
   */
  def apply(pos: Position): Option[Tile] = apply(pos._1, pos._2)

  /**
   * Obtain the shortest path from <code>from</code> to <code>to</code>.
   * @return None if no shortest path could be found, Some(path) otherwise.  
   */
  def getPath(from: Position, to: Position): Option[Path] = {
    pathfinder.path(internal(from._1, from._2), internal(to._1, to._2)) match {
      case None => None
      case Some(p) => Some(p map external)
    }
  }

}